package cn.kinoko.common.aspect;

import cn.kinoko.common.aspect.annotation.Lock;
import cn.kinoko.common.constant.HeaderConstant;
import cn.kinoko.common.utils.ExceptionUtil;
import cn.kinoko.common.utils.SpelUtil;
import cn.kinoko.common.utils.ThreadLocalUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * 分布式缓存切面
 *
 * @author kk
 */
@Aspect
@Slf4j
@Component
public class LockAspect {

    // 存储结构为 key -> (key, value) 为实现过期时间
    @Resource
    private Cache<String, Cache<String, Object>> lockCache;

    @Around("@annotation(lock)")
    public Object around(ProceedingJoinPoint joinPoint, Lock lock) throws Throwable {
        // 获取redisKey
        String redisKey = SpelUtil.getRedisKey(joinPoint, lock.key(), lock.params());
        if (lock.currentUser()) {
            redisKey += (":" + ThreadLocalUtil.get(HeaderConstant.UID_HEADER));
        }
        // 尝试获取锁，若获取失败则抛出异常
        ExceptionUtil.assertion(!tryLock(lock, redisKey), lock.errorDesc());
        Object target;
        try {
            // 执行业务方法
            target = joinPoint.proceed();
        } finally {
            // 释放锁
            lockCache.invalidate(redisKey);
        }
        return target;
    }

    /**
     * 获取锁同步，拟Redis setNX指令单线程特性
     *
     * @param lock     锁注解
     * @param redisKey 缓存key
     * @return true 获取锁成功，false获取锁失败
     */
    private synchronized boolean tryLock(Lock lock, String redisKey) {
        // 获取锁
        Cache<String, Object> keyCache = lockCache.getIfPresent(redisKey);
        // 获取锁失败则返回错误提示
        if (keyCache != null && keyCache.getIfPresent(redisKey) != null) return false;
        // 构建lock
        if (keyCache == null) {
            keyCache = Caffeine.newBuilder()
                    .maximumSize(1)
                    .expireAfterWrite(lock.expire(), lock.timeUnit())
                    .build();
        }
        // 加入过期缓存，value无所谓，所以随便放一个值
        keyCache.put(redisKey, redisKey);
        // 将lock放入缓存
        lockCache.put(redisKey, keyCache);
        return true;
    }

}
